﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using UnDbxNET.Files;

namespace UnDbxNET
{
    public delegate void UpdateLogEventHandler(string e);
    public delegate void ProgressEventHandler(int e);
    public delegate void ProcessFinishedEventHandler();

    class undbx
    {
        public event UpdateLogEventHandler ReportLogUpdate;
        public event ProgressEventHandler ReportProgress;
        public event ProcessFinishedEventHandler ReportProcessFinished;

        public enum dbx_save_status_t
        {
            DBX_SAVE_NOOP,
            DBX_SAVE_OK,
            DBX_SAVE_ERROR
        }

        private void SetMessageFileTime(dbxInfo pDbxInfo, String pDir)
        {
            try
            {
                DateTime MyDate = (pDbxInfo.send_create_time != null) ? pDbxInfo.send_create_time : pDbxInfo.receive_create_time;
                FileInfo MyFile = new FileInfo(Path.Combine(pDir, pDbxInfo.filename));
                MyFile.CreationTime = MyDate;
            }
            catch (Exception ex)
            {
                OnLogUpdate("ERROR - SetMessageFileTime: " + ex.Message);
            }            
        }

        private void SetMessageTime(String pFileName)
        {
            try
            {
                FileInfo MyFile = new FileInfo(pFileName);
                MyFile.CreationTime = DateTime.Now;
            }
            catch (Exception ex)
            {
                OnLogUpdate("ERROR - SetMessageTime: " + ex.Message);
            }
        }

        private dbx_save_status_t SaveMessage(String pPath, byte[] pMessage)
        {
            dbx_save_status_t MyResult = dbx_save_status_t.DBX_SAVE_OK;

            try
            {
                using (StreamWriter mystream = new StreamWriter(pPath))
                {
                    mystream.BaseStream.Write(pMessage, 0, pMessage.Length);
                }
            }
            catch (Exception)
            {
                MyResult = dbx_save_status_t.DBX_SAVE_ERROR;
            }            

            return MyResult;
        }

        private dbx_save_status_t MaybeSaveMessage(dbxFile pDbxFile, int pMessage, String pDir, bool pForce)
        {
            dbx_save_status_t MyResult = dbx_save_status_t.DBX_SAVE_NOOP;
            dbxInfo MyDbxInfo = pDbxFile.info[pMessage];
            long size = 0;
            int message_size = 0;

            if (!pForce) size = new FileInfo(Path.Combine(pDir, MyDbxInfo.filename)).Length;

            if (pForce || (MyDbxInfo.valid & dbxEnums.dbx_mask_t.DBX_MASK_MSGSIZE) == 0 || size != MyDbxInfo.message_size)
            {
                byte[] MyMessage = pDbxFile.dbxMessage(pMessage, message_size); 
                if (pForce || (size != message_size))
                {
                    if (pDir.Length >= 248) throw new NotSupportedException("Proszę zmienić ścięzkę docelową");

                    if (pDir.Length + MyDbxInfo.filename.Length > 260)
                    {
                        int MyLen = pDir.Length + MyDbxInfo.filename.Length - 260;
                        String MyNewFileName = MyDbxInfo.filename.Substring(0, MyDbxInfo.filename.IndexOf(".eml") - MyLen) + ".eml";
                        MyDbxInfo.filename = MyNewFileName;
                    }

                    String MyPath = Path.Combine(pDir, MyDbxInfo.filename);
                    MyResult = SaveMessage(MyPath, MyMessage);
                    switch (MyResult)
                    {
                        case dbx_save_status_t.DBX_SAVE_ERROR:
                            OnLogUpdate("   Unknown ERROR while saving message: " + MyDbxInfo.filename);
                            break;
                        case dbx_save_status_t.DBX_SAVE_OK:
                            SetMessageFileTime(MyDbxInfo, pDir);
                            OnLogUpdate("    Saved message: " + MyDbxInfo.filename);
                            break;
                        default:
                            break;
                    }
                }
            }

            return MyResult;
        }

        private void Recovery(dbxFile pDbxFile, String pOutDir)
        {
            int size = 0;
            int count = 0;
            int innerCount = 0;
            for (int i = 0; i < (int)dbxEnums.dbx_scan_t.DBX_SCAN_NUM; i++)
                count += pDbxFile.scan[i].count;


            for (int i = 0; i < (int) dbxEnums.dbx_scan_t.DBX_SCAN_NUM; i++) // ??
            {
                dbx_save_status_t MyStatus = dbx_save_status_t.DBX_SAVE_NOOP;

                if (pDbxFile.scan[i].count > 0)
                {
                    String MyDestDir = Path.Combine(pOutDir, "recovered");
                    Directory.CreateDirectory(MyDestDir);

                    for (int imessage = 0; imessage < pDbxFile.scan[i].count; imessage++)
                    {
                        String MyFileName = string.Empty;
                        DateTime MyTimeStamp;

                        byte[] MyMessage = pDbxFile.dbxRecoverMessage(i, imessage, out MyTimeStamp, out size, out MyFileName);
                        if (!MyMessage.Any()) continue;

                        String MyPath = Path.Combine(MyDestDir, MyFileName);
                        MyStatus = SaveMessage(MyPath, MyMessage);
                        switch (MyStatus)
                        {
                            case dbx_save_status_t.DBX_SAVE_ERROR:
                                OnLogUpdate("    Unknown ERROR while recovering: " + MyFileName);
                                break;
                            case dbx_save_status_t.DBX_SAVE_OK:
                                OnLogUpdate("    Recovered: " + MyFileName);
                                SetMessageTime(MyPath);
                                break;
                            default:
                                break;
                        }

                        OnInnerProgressChanged((int)((double)++innerCount / count * 100));
                    }
                }
            }
        }

        private void ExtractDbxFile(dbxFile pDbxFile, String pOutDir)
        {
            int iMessage = 0;
            int iFiles = 0;
            bool MyNoMoreMsg = false;
            bool MyNoMoreFiles = false;

            OnLogUpdate("Extracting starts");

            Func<String, String> lambda = path => new FileInfo(path).Name;
            List<String> MyFileList = Directory.GetFiles(pOutDir, "*.eml").Select(lambda).ToList();
            MyFileList.Sort();

            MyNoMoreMsg = (iMessage == pDbxFile.message_count);
            MyNoMoreFiles = (iFiles == MyFileList.Count);

            int MyCount = pDbxFile.message_count;
            while (!MyNoMoreMsg || !MyNoMoreFiles)
            {
                dbx_save_status_t MyStatus = dbx_save_status_t.DBX_SAVE_NOOP;
                int MyCond = 0;

                if (!MyNoMoreMsg && !MyNoMoreFiles)
                    MyCond = String.Compare(pDbxFile.info[iMessage].filename, MyFileList[iFiles]);
                else if (MyNoMoreFiles)
                    MyCond = -1;
                else
                    MyCond = 1;


                if (MyCond < 0)
                {
                    MyStatus = MaybeSaveMessage(pDbxFile, iMessage, pOutDir, true);
                    iMessage++;
                    MyNoMoreMsg = (iMessage == pDbxFile.message_count);
                }
                else if (MyCond == 0)
                {
                    MyStatus = MaybeSaveMessage(pDbxFile, iMessage, pOutDir, false);
                    iMessage++;
                    MyNoMoreMsg = (iMessage == pDbxFile.message_count);
                    iFiles++;
                    MyNoMoreFiles = (iFiles == MyFileList.Count);
                }
                else
                {
                    /* file on disk not found in dbx: delete from disk */
                    File.Delete(Path.Combine(pOutDir, MyFileList[iFiles]));
                    iFiles++;
                    MyNoMoreFiles = (iFiles == MyFileList.Count);
                }

                OnInnerProgressChanged((int)((double)iMessage / MyCount * 100));
            }
        }

        private void unDbxFile(FileInfo pDbxFile, String pOutDir, bool pRecover)
        {
            dbxFile MyDbxFile = new dbxFile(pDbxFile, pRecover, dbxfile_LogMessage, dbxFile_Progress, dbxFile_ProcessFinished);
            if (!pRecover)
            {
                if (MyDbxFile.type != dbxEnums.dbx_type_t.DBX_TYPE_EMAIL)
                {
                    OnLogUpdate(string.Format("DBX file {0} does not contain messages", MyDbxFile.m_file.Name));
                    return;
                }

                if (MyDbxFile.m_file.Length >= 0x80000000)
                {
                    OnLogUpdate(string.Format("DBX file {0} is corrupted (larger than 2GB)", MyDbxFile.m_file.Name));
                    return;
                }
            }
            
            if (pRecover)
                Recovery(MyDbxFile, pOutDir);
            else
                ExtractDbxFile(MyDbxFile, pOutDir);
        }

        public void unDbx(string pDbxDir, string pOutDir,bool pRecover)
        {
            DirectoryInfo MyDbxDir = new DirectoryInfo(pDbxDir);

            if (!MyDbxDir.Exists)
            {
                //throw new Exception("Directory does not exist");
                OnLogUpdate("ERROR: Directory does not exist");
                return;
            }

            FileInfo[] MyFiles = MyDbxDir.GetFiles("*.dbx", SearchOption.TopDirectoryOnly);

            DbxFilesCount = MyFiles.Length;
            CurrentFile = 0;
            foreach (FileInfo MyDbxFile in MyFiles)
            {
                CurrentFile++;
                OnLogUpdate("  Processing DBX file: " + MyDbxFile.Name);
                unDbxFile(MyDbxFile, pOutDir, pRecover);
            }

            OnProcessFinished();
        }

        private void dbxfile_LogMessage(string e)
        {
            OnLogUpdate(e);
        }

        private void dbxFile_Progress(int e)
        {
            OnTotalProgressChanged(e);
        }

        private void dbxFile_ProcessFinished()
        {
            OnTotalProgressChanged(100);
            OnTotalProgressChanged(0);
        }

        private void OnInnerProgressChanged(int e)
        {
            if (DbxFilesCount == 0)
                OnProcessFinished();
            else
            {
                int MyProgress = (int)(((double)e / DbxFilesCount) * CurrentFile);
                OnTotalProgressChanged(MyProgress);
            }
        }

        private void OnTotalProgressChanged(int e)
        {
            if (ReportProgress != null) this.ReportProgress(e);
        }

        private void OnProcessFinished()
        {
            if (ReportProcessFinished != null) this.ReportProcessFinished();
        }

        private void OnLogUpdate(string e)
        {
            if (ReportLogUpdate != null) this.ReportLogUpdate(e);
        }

        private int DbxFilesCount = 0;
        private int CurrentFile = 0;
    }
}
